import qs from 'qs';
import axios, { AxiosInstance, Method, AxiosRequestConfig } from 'axios';

interface HttpRequestOptions<T = any, U = T> {
  svr?: string;
  path?: string;
  url?: string;
  data?: Record<string, any> | string;
  headers?: Record<string, string>;
  transform?: (data: T) => U;
  method?: Method;
}

function noTransform<T, U = T>(data: T): U {
  return data as unknown as U;
}

export interface BaseHttpQueryOptions {
  svr?: string;
}

interface HttpGetNormalizeOptions<T = any, U = T> extends Omit<HttpGetOptions, 'transform'> {
  transform: (data: T) => U;
}

interface HttpPostNormalizeOptions<T = any, U = T> extends Omit<HttpPostOptions, 'transform'> {
  transform: (data: T) => U;
  type: BodyType;
}

export interface HttpGetOptions<T = any, U = T> extends Omit<HttpRequestOptions<T, U>, 'method'> {
  query?: Record<string, any>;
}

export interface HttpPostOptions<T = any, U = T> extends Omit<HttpRequestOptions<T, U>, 'method'> {
  type?: BodyType | null;
  serialize?: (data: T) => any;
  query?: Record<string, any>;
}

export interface HttpDeleteOptions<T = any, U = T> extends HttpGetOptions<T, U> {
  type?: BodyType | null;
}

export interface HttpPutOptions<T = any, U = T> extends HttpPostOptions<T, U> {}

export interface HttpHeadOptions<T = any, U = T> extends HttpGetOptions<T, U> {}

export enum BodyType {
  FORM = 'application/x-www-form-urlencoded;charset=utf-8',
  RAW = 'application/x-www-form-urlencoded;charset=utf-8',
  JSON = 'application/json;charset=utf-8',
  FORM_DATA = 'multipart/form-data',
}

function normalizeOptions<T = any, U = T>(options: HttpGetOptions<T, U>): HttpGetNormalizeOptions<T, U> {
  if (!options.transform) {
    options.transform = noTransform;
  }
  return {
    ...options,
    transform: options.transform,
  };
}

function normalizePostOptions<T = any, U = T>(options: HttpPostOptions<T, U>): HttpPostNormalizeOptions<T, U> {
  if (!options.transform) {
    options.transform = noTransform;
  }
  return {
    ...options,
    type: options.type || BodyType.FORM,
    transform: options.transform,
  };
}

/**
 * 转换参数
 * @param data
 * @returns {string}
 */
function buildQuery(data: object) {
  if (data) {
    if (data instanceof FormData) {
      return data;
    }
    if (typeof data === 'string') {
      return data;
    }
    if (Array.isArray(data)) {
      return data
        .filter(([key, value]) => typeof value !== 'undefined')
        .map(([key, value]) => {
          return `${key}=${encodeURIComponent(value)}`;
        })
        .join('&');
    }
    if (typeof data === 'object') {
      return Object.entries(data)
        .filter(([key, value]) => typeof value !== 'undefined')
        .map(([key, value]) => {
          return `${key}=${encodeURIComponent(value)}`;
        })
        .join('&');
    }
  }
  return '';
}

export default class Http {
  public static BodyType = BodyType;
  public static buildQuery = buildQuery;

  public static create(config?: AxiosRequestConfig) {
    return new Http(config);
  }

  public static request<T = any, U = T>(options: HttpRequestOptions<T, U>, axiosIns = Http.axios()) {
    const { method } = options;
    if (method === 'POST') {
      return Http.post(options, axiosIns);
    }
    return Http.get(options, axiosIns);
  }

  /**
   * GET
   * @param {HttpGetOptions} options
   * @param axiosIns
   * @return {Promise<any>}
   */
  public static get<T = any, U = T>(options: HttpGetOptions<T, U> | string, axiosIns = Http.axios()): Promise<U> {
    return Http.reqNoBody<HttpGetOptions, T, U>(axiosIns, options, 'get');
  }

  /**
   * HEAD
   * @param {HttpGetOptions} options
   * @param axiosIns
   * @return {Promise<any>}
   */
  public static head<T = any, U = T>(options: HttpHeadOptions<T, U> | string, axiosIns = Http.axios()): Promise<U> {
    return Http.reqNoBody<HttpHeadOptions, T, U>(axiosIns, options, 'head');
  }

  /**
   * POST
   * @param {HttpPostOptions} options
   * @param axiosIns
   * @return {Promise<any>}
   */
  public static post<T = any, U = T>(options: HttpPostOptions<T, U>, axiosIns = Http.axios()) {
    return Http.req<HttpPostOptions, T, U>(axiosIns, options, 'post');
  }

  /**
   * DELETE
   * @param {HttpDeleteOptions} options
   * @param axiosIns
   * @return {Promise<any>}
   */
  public static delete<T = any, U = T>(options: HttpDeleteOptions<T, U>, axiosIns = Http.axios()) {
    return Http.reqNoBody<HttpDeleteOptions, T, U>(axiosIns, options, 'delete');
  }

  /**
   * PUT
   * @param {HttpPutOptions} options
   * @param axiosIns
   * @return {Promise<any>}
   */
  public static put<T = any, U = T>(options: HttpPutOptions<T, U>, axiosIns = Http.axios()) {
    return Http.req<HttpPutOptions, T, U>(axiosIns, options, 'put');
  }

  public static axios(config?: AxiosRequestConfig) {
    return axios.create(config);
  }

  public static url(options: HttpGetOptions) {
    const { url: requestUrl, svr = '', path = '', data } = normalizeOptions(options);
    const url = requestUrl ? requestUrl : Http.getURL(svr, path);
    return Http.axios().getUri({ url, params: data });
  }

  public static getURL(svr: string, path: string) {
    let basePath = '';
    if (/\/$/.test(basePath)) {
      basePath = basePath.slice(0, -1);
    }
    return [basePath, /^\//.test(path) ? path.slice(1) : path].filter(Boolean).join('/');
  }

  private static reqNoBody<K extends HttpGetOptions<T, U>, T = any, U = T>(
    axiosIns: AxiosInstance,
    options: K | string,
    method: 'head' | 'get' | 'delete'
  ) {
    if (typeof options === 'string') {
      return Http.get<T, U>({ url: options });
    }
    const { url: requestUrl, svr = '', path = '', data, transform, query, headers } = normalizeOptions(options);
    const url = requestUrl ? requestUrl : Http.getURL(svr, path);
    return axiosIns[method]<T>(url, {
      params: {
        ...data,
        ...query,
      },
      paramsSerializer: qs.stringify,
      headers: { ...headers },
    })
      .then((res) => res.data)
      .then((res) => transform(res));
  }

  private static req<K extends HttpPostOptions<T, U>, T = any, U = T>(
    axiosIns: AxiosInstance,
    options: K,
    method: 'put' | 'post'
  ) {
    const {
      url: requestUrl,
      svr = '',
      path = '',
      data,
      transform,
      type: contentType = BodyType.FORM,
      headers,
      query,
      serialize,
    } = normalizePostOptions(options);
    const url = requestUrl ? requestUrl : Http.getURL(svr, path);
    let body: any;
    switch (contentType) {
      case BodyType.RAW:
        body = data;
        break;
      case BodyType.FORM:
        body = qs.stringify({
          ...data,
        });
        break;
      case BodyType.JSON:
        body = JSON.stringify(data);
        break;
      case BodyType.FORM_DATA:
        body = new FormData();
        Object.keys({ ...data }).forEach((k) => {
          const value = data![k];
          (Array.isArray(value) ? value : [value]).forEach((l) => body.append(k, l));
        });
        break;
      default:
        body = serialize ? serialize(data) : data;
        break;
    }
    return axiosIns[method]<T>(url, body, {
      params: { ...query },
      headers: { 'content-type': contentType, ...headers },
    })
      .then((res) => res.data)
      .then((res) => transform(res));
  }

  private readonly axiosIns: AxiosInstance;

  public get axios() {
    return this.axiosIns;
  }

  constructor(config?: AxiosRequestConfig) {
    this.axiosIns = Http.axios(config);
  }

  get<T = any, U = T>(options: HttpGetOptions<T, U> | string) {
    return Http.get<T, U>(options, this.axiosIns);
  }

  post<T = any, U = T>(options: HttpPostOptions<T, U>) {
    return Http.post<T, U>(options, this.axiosIns);
  }

  put<T = any, U = T>(options: HttpPutOptions<T, U>) {
    return Http.put<T, U>(options, this.axiosIns);
  }

  head<T = any, U = T>(options: HttpHeadOptions<T, U> | string) {
    return Http.head<T, U>(options, this.axiosIns);
  }

  delete<T = any, U = T>(options: HttpDeleteOptions<T, U>) {
    return Http.delete<T, U>(options, this.axiosIns);
  }
}
